package com.gitee.qdbp.base.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.gitee.qdbp.able.exception.ServiceException;
import com.gitee.qdbp.able.result.ResultCode;
import com.gitee.qdbp.base.system.model.IAccount;
import com.gitee.qdbp.general.common.api.files.model.IPathParams;
import com.gitee.qdbp.general.common.api.files.model.PathParams;
import com.gitee.qdbp.general.common.api.files.service.IWebFileService;
import com.gitee.qdbp.general.common.error.FileErrorCode;
import com.gitee.qdbp.tools.files.FileTools;
import com.gitee.qdbp.tools.files.PathTools;
import com.gitee.qdbp.tools.utils.PropertyTools;
import com.gitee.qdbp.tools.utils.StringTools;
import com.gitee.qdbp.tools.utils.VerifyTools;

/**
 * WEB文件上传下载处理工具
 *
 * @author zhaohuihua
 * @version 171130
 */
public class WebFileHandler {

    private static final Logger log = LoggerFactory.getLogger(WebFileHandler.class);

    private IWebFileService webFileService;

    public WebFileHandler(IWebFileService webFileService) {
        this.webFileService = webFileService;
    }

    /**
     * 生成保存文件的参数对象
     * 
     * @param folder 根文件夹, 可为空(默认为files)
     * @param user 当前登录用户, 有可能为空
     * @param extra 附加参数, 用于替换文件路径和文件名中的占位符
     * @return
     */
    public static IPathParams generatePathParams(String folder, IAccount user, Map<String, Object> extra) {
        Map<String, Object> map = new HashMap<>();
        if (user != null) {
            map.put("tenant-code", user.getTenantCode());
            map.put("user-id", user.getId());
        }
        if (VerifyTools.isNotBlank(extra)) {
            map.putAll(extra);
        }
        PathParams options = new PathParams();
        options.setFolder(folder);
        options.setExtra(map);
        return options;
    }

    /**
     * 生成保存文件的参数对象
     * 
     * @param folder 根文件夹, 可为空(默认为files)
     * @param user 当前登录用户, 有可能为空
     * @param extra 附加参数, 用于替换文件路径和文件名中的占位符
     * @return
     */
    public static IPathParams generatePathParams(String folder, IAccount user, String extra) {
        JSONObject json = null;
        if (VerifyTools.isNotBlank(extra)) {
            try {
                json = JSON.parseObject(extra);
            } catch (Exception e) {
                log.error("File upload, extra params can't parse, {}, {}", e.toString(), extra);
            }
        }
        return generatePathParams(folder, user, json);
    }

    /**
     * 发现并上传文件<br>
     * 每个需要上传的文件, 在前端构建两个input<br>
     * 一个type="file"用于上传文件流, 一个type="hidden", 用于保存修改前的图片地址; 用序号一一对应<br>
     * &lt;p&gt; &lt;input type="file" name="files0"/&gt; &lt;input type="hidden" name="images0"/&gt; &lt;p&gt;<br>
     * &lt;p&gt; &lt;input type="file" name="files1"/&gt; &lt;input type="hidden" name="images1"/&gt; &lt;p&gt;<br>
     * &lt;p&gt; &lt;input type="file" name="files2"/&gt; &lt;input type="hidden" name="images2"/&gt; &lt;p&gt;<br>
     * &lt;p&gt; &lt;input type="file" name="files3"/&gt; &lt;input type="hidden" name="images3"/&gt; &lt;p&gt;<br>
     * &lt;p&gt; &lt;input type="file" name="files4"/&gt; &lt;input type="hidden" name="images4"/&gt; &lt;p&gt;<br>
     * 后台调用findAndUploadFiles(request, "images", "files")查找图片<br>
     * 图片列表是有顺序的<br>
     * 因此如果5张图片, 其中1/2(序号从0开始)修改了, 那么仍然应该是5条记录<br>
     * 其中0/3/4的type="hidden"是原图片URL,type="file"为null, 1/2的type="hidden"是新图片的原URL,type="file"为新图片输入流<br>
     * images0/images1/images2/images3/images4=原图片URL; files1/files2=文件流<br>
     * 此时应将files1/files2上传至文件服务器, 取得URL, 替换到images1/images2的位置<br>
     * <br>
     * 另外, 如果文件直接上传至文件服务器而不通过JAVA服务器中转<br>
     * 那么在前端就已已经把images1/images2替换成新图片地址了<br>
     * 应将type="file"设为disabled, 相当于每次type="file"都为null, 然后这个处理逻辑也就没有问题, 可以兼容了<br>
     * 
     * @param request
     * @param imageFieldPrefix 图片字段的前缀
     * @param fileFieldPrefix 文件字段的前缀
     * @return 上传后的图片地址
     * @throws ServiceException
     */
    public List<String> findAndUploadFiles(HttpServletRequest request, String imageFieldPrefix, String fileFieldPrefix)
            throws ServiceException {

        String folder = request.getParameter("folder");
        String filename = request.getParameter("filename");
        String extra = request.getParameter("extra");

        int size = 100;
        int max = -1;
        String[] urls = new String[size];
        for (int i = 0; i < size; i++) {
            // images0, images3, images4 原图片URL
            String url = request.getParameter(imageFieldPrefix + i);
            if (VerifyTools.isNotBlank(url)) {
                urls[i] = url;
                if (max < i) {
                    max = i;
                }
            }
        }
        if (request instanceof MultipartHttpServletRequest) {
            MultipartHttpServletRequest mrequest = (MultipartHttpServletRequest) request;
            for (Entry<String, MultipartFile> entry : mrequest.getFileMap().entrySet()) {
                if (entry.getKey().startsWith(fileFieldPrefix)) {
                    // files1, files2 文件流
                    String suffix = entry.getKey().substring(fileFieldPrefix.length());
                    if (VerifyTools.isNotBlank(suffix) && StringTools.isDigit(suffix)) {
                        int i = Integer.valueOf(suffix);
                        if (i < size && entry.getValue().getSize() > 0) {
                            urls[i] = upload(entry.getValue(), folder, filename, extra);
                        }
                        if (max < i) {
                            max = i;
                        }
                    }
                }
            }
        }
        List<String> list = new ArrayList<>();
        for (int i = 0; i <= max; i++) {
            list.add(urls[i]);
        }
        return list;
    }

    /**
     * 文件上传<br>
     * 文件扩展名必须在allow.upload.suffix配置中<br>
     * 
     * @param file 上传的文件内容
     * @param folder 根文件夹, 可为空(默认为files)
     * @param filename 文件名, 用来获取文件扩展名
     * @param extra 附加参数, 用于替换文件路径和文件名中的占位符
     * @return 上传后的URL路径
     */
    public String upload(MultipartFile file, String folder, String filename, Map<String, Object> extra)
            throws ServiceException {
        IAccount user = SessionTools.checkLoginUser();
        IPathParams options = generatePathParams(folder, user, extra);
        return upload(file, filename, options);
    }

    /**
     * 文件上传<br>
     * 文件扩展名必须在allow.upload.suffix配置中<br>
     * 
     * @param file 上传的文件内容
     * @param folder 根文件夹, 可为空(默认为files)
     * @param filename 文件名, 用来获取文件扩展名
     * @param extra 附加参数, 用于替换文件路径和文件名中的占位符
     * @return 上传后的URL路径
     */
    public String upload(MultipartFile file, String folder, String filename, String extra) throws ServiceException {
        IAccount user = SessionTools.checkLoginUser();
        IPathParams options = generatePathParams(folder, user, extra);
        return upload(file, filename, options);
    }

    /**
     * 文件上传<br>
     * 文件扩展名必须在allow.upload.suffix配置中<br>
     * 
     * @param file 上传的文件内容
     * @param filename 文件名, 用来获取文件扩展名
     * @param options 附加参数, 用于替换文件路径和文件名中的占位符
     * @return 上传后的URL路径
     */
    public String upload(MultipartFile file, String filename, IPathParams options) throws ServiceException {

        Properties setting = WebTools.me.getContextProperties();
        if (filename == null) {
            filename = file.getName();
        }
        if (!isAllowExtension(setting, filename, "allow.upload.suffix", "allow.download.suffix")) {
            throw new ServiceException(FileErrorCode.FILE_FORMAT_NONSUPPORT);
        }
        InputStream input;
        try {
            input = file.getInputStream();
        } catch (IOException e) {
            throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e);
        }
        long size = file.getSize();
        return webFileService.upload(options, filename, input, size);
    }

    /**
     * 下载文件<br>
     * 文件扩展名必须在allow.download.suffix配置中<br>
     * 文件必须位于文件服务配置的文件夹或allow.download.folder配置中<br>
     * 
     * @param path 文件的保存路径(不可为空)
     * @param filename 下载时浏览器提示的文件名(可为空,从保存路径中截取)
     * @throws IOException
     * @throws ServiceException
     */
    public void download(HttpServletRequest request, HttpServletResponse response, String path, String filename)
            throws ServiceException {

        if (VerifyTools.isBlank(path)) {
            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED);
        }

        if (VerifyTools.isBlank(filename)) {
            filename = new File(path).getName();
        }

        Properties setting = WebTools.me.getContextProperties();
        // 1.获取要下载的文件的绝对路径
        String realPath = getRealPath(setting, path);

        if (!new File(realPath).exists()) {
            log.warn("FileNotFound: {}", realPath);
            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
        }

        try {
            // 2.设置content-disposition响应头控制浏览器以下载的形式打开文件
            response.setHeader("content-disposition", FileIoTools.toAttachmentDesc(request, filename));
        } catch (Exception e) {
            String extension = VerifyTools.nvl(PathTools.getExtension(filename), ".tmp");
            response.setHeader("content-disposition", "attachment;filename=file" + extension);
        }

        // 3.获取要下载的文件输入流
        try (InputStream in = new FileInputStream(realPath);) {
            // 3.1.通过response对象获取OutputStream流
            OutputStream out = response.getOutputStream();
            // 3.2.复制文件流数据
            FileTools.copy(in, out);
        } catch (IOException e) {
            throw new ServiceException(FileErrorCode.FILE_DOWNLOAD_ERROR, e);
        }

    }

    // 获取文件实际保存路径
    // 如果是download.location.xxx指定的路径, 根据配置获取文件
    // 如果是文件服务保存的, 根据文件服务配置获取文件
    // 否则获取WEB服务根路径下的文件
    private String getRealPath(Properties setting, String path) throws ServiceException {

        if (!StringTools.isUrl(path)) {
            String savePath = getSpecialLocationPath(setting, path);
            if (savePath != null) {
                return savePath;
            }
        }

        // 先判断文件路径是不是文件服务的路径
        String fileCenterPath = webFileService.toLocalAbsolutePath(path);
        if (fileCenterPath != null) {
            return fileCenterPath;
        }

        // WebRoot
        String serverPath = WebTools.me.getWebRoot();
        if (StringTools.isUrl(path)) {
            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR);
        }
        if (!isAllowDownload(setting, path, "web", true)) {
            throw new ServiceException(ResultCode.FORBIDDEN);
        }
        return PathTools.concat(serverPath, path);
    }

    private static final Pattern FIRST_FOLDER_NAME = Pattern.compile("\\s*[/\\\\]?([^/\\\\]+)[/\\\\](.*+)\\s*");
    private static final Pattern DOWNLOAD_LOCATION_CONFIG = Pattern.compile("\\s*(?:(alias|root)\\s+)?(.*+)\\s*");

    // 通过配置指定允许下载指定文件夹中的文件
    // 如果: download.location.xxx = alias /home/tomcat/runtimelogs/
    // 那么: path=xxx/web-user-error.log = /home/tomcat/runtimelogs/web/user-error.log文件
    // 如果: download.location.xxx =  root /home/tomcat/runtimelogs/
    // 那么: path=xxx/web/user-error.log = /home/tomcat/runtimelogs/xxx/web/user-error.log文件
    // 完整配置
    // download.location.xxx = alias /home/tomcat/runtimelogs/
    // allow.download.suffix.location.xxx = .log|.err|.txt
    private String getSpecialLocationPath(Properties setting, String path) throws ServiceException {
        String folder;
        String file;
        {
            Matcher matcher = FIRST_FOLDER_NAME.matcher(path);
            if (matcher.matches()) {
                folder = matcher.group(1);
                file = matcher.group(2);
            } else {
                return null;
            }
        }

        String type;
        String root;
        {
            String string = PropertyTools.getString(setting, "download.location." + folder, false);
            if (VerifyTools.isBlank(string)) {
                return null;
            }
            Matcher matcher = DOWNLOAD_LOCATION_CONFIG.matcher(string);
            if (matcher.matches()) {
                type = matcher.group(1);
                root = matcher.group(2);
            } else {
                return null;
            }
        }

        if (!isAllowDownload(setting, path, "location." + folder, false)) {
            throw new ServiceException(ResultCode.FORBIDDEN);
        }

        if (VerifyTools.isBlank(type) || "alias".equals(type)) {
            return PathTools.concat(root, file);
        } else if ("root".equals(type)) {
            return PathTools.concat(root, folder, file);
        } else {
            return null;
        }
    }

    private boolean isAllowExtension(Properties setting, String path, String key, String... keys) {
        String string = PropertyTools.getStringUseDefKeys(setting, key, keys);
        return isAllowExtension(path, StringTools.split(string));
    }

    /** 判断是否允许下载 **/
    // default             -- allow.download.suffix
    // type = web          -- allow.download.suffix.web
    // type = filecenter   -- allow.download.suffix.filecenter
    // location default    -- allow.download.suffix.location
    // type = location.xxx -- allow.download.suffix.location.xxx
    private boolean isAllowDownload(Properties setting, String path, String type, boolean checkFolder) {
        String suffixes = PropertyTools.getStringUseSuffix(setting, "allow.download.suffix", type);
        boolean allowSuffix = isAllowExtension(path, StringTools.split(suffixes));

        boolean allowFolder = true;
        // WebRoot下的文件需要判断文件夹
        if (checkFolder) {
            String string = PropertyTools.getStringUseSuffix(setting, "allow.download.folder", type);
            String[] folders = StringTools.split(string);
            if (folders == null || folders.length == 0) {
                folders = new String[] { "assets" }; // 兼容旧版本
            }
            allowFolder = isAllowFolder(path, folders);
        }

        return allowSuffix && allowFolder;
    }

    private boolean isAllowExtension(String path, String[] suffixes) {
        // 判断文件名后缀
        if (VerifyTools.isNotBlank(suffixes)) {
            for (String suffix : suffixes) {
                if ("*".equals(suffix)) {
                    return true;
                }
                if (path.endsWith(suffix)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean isAllowFolder(String path, String[] folders) {
        if (VerifyTools.isNotBlank(folders)) {
            for (String folder : folders) {
                if ("*".equals(folder)) {
                    return true;
                }
                if (path.startsWith(folder + "/") || path.startsWith("/" + folder + "/")) {
                    return true;
                }
            }
        }
        return false;
    }
}
